package cn.me.alphamq.common.util;

import cn.me.alphamq.common.constant.GlobalConstant;
import cn.me.alphamq.common.msg.TcpMsg;
import cn.me.alphamq.common.msg.TcpMsgCodec;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;

/**
 * netty 的工具类
 *
 * @author f
 */
public class NettyUtil {

    private static final Logger log = LoggerFactory.getLogger(NettyUtil.class);

    /**
     * 从 channel 的上下文中获取网络地址
     *
     * @param ctx channel 的上下文
     * @return 网络地址
     */
    public static String getAddressFromCtx(ChannelHandlerContext ctx) {
        InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
        return address.getHostString() + ":" + address.getPort();
    }

    /**
     * 设置 ctx 的唯一标识
     *
     * @param ctx     待设置的 ctx
     * @param address ctx 所属机器的网络地址
     */
    public static void setCtxId(ChannelHandlerContext ctx, String address) {
        ctx.attr(AttributeKey.valueOf(GlobalConstant.CTX_ATTR_NAME)).set(address);
    }

    /**
     * 获取 ctx 的唯一标识
     *
     * @param ctx 待获取唯一标识的 ctx
     * @return ctx 所属机器的唯一标识
     */
    public static String getCtxId(ChannelHandlerContext ctx) {
        return (String) ctx.attr(AttributeKey.valueOf(GlobalConstant.CTX_ATTR_NAME)).get();
    }

    /**
     * 开启 netty 的服务端，启动完服务端就会返回
     *
     * @param port       服务端监听的端口
     * @param serverName 服务器的名称，用于打印日志
     * @param handler    服务端对 TcpMsg 的处理器，注意：这里的 handler 要求实现 @ChannelHandler.Sharable 接口
     */
    public static void startNettyServer(int port, String serverName, SimpleChannelInboundHandler<TcpMsg> handler) {
        checkHandler(handler);

        // bossGroup 负责处理 accept 事件
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        // workerGroup 负责处理 read&write 事件
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        // 构建并配置 serverBootstrap
        ServerBootstrap serverBootstrap = new ServerBootstrap()
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        // TODO 是否要添加一个解决 粘包 问题的处理器
                        ch.pipeline()
                                .addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 6, 4, 0, 0))
                                .addLast(new TcpMsgCodec())
                                .addLast(handler);
                    }
                });

        // 在 JVM 关闭的时候，将 bossGroup 和 workerGroup 也关闭掉
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            log.info("{} stopped...", serverName);
        }));

        // 启动服务器，监听指定端口
        try {
            serverBootstrap.bind(port).sync();
            log.info("{} started on port {}...", serverName, port);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 开启 netty 的客户端，与服务端建立连接就会返回
     *
     * @param ip         待连接的服务端的 ip 地址
     * @param port       待连接的服务端的 端口号
     * @param clientName 客户端名称，用于打印日志
     * @param handler    客户端对 TcpMsg 的处理器，注意：这里的 handler 要求实现 @ChannelHandler.Sharable 接口
     * @return 返回与服务端通信的 channel
     */
    public static Channel startNettyClient(String ip, int port, String clientName,
                                           SimpleChannelInboundHandler<TcpMsg> handler) {
        checkHandler(handler);

        // 客户端的 netty 线程池
        EventLoopGroup clientGroup = new NioEventLoopGroup();

        // 构建并配置 bootstrap
        Bootstrap bootstrap = new Bootstrap()
                .group(clientGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        // TODO 是否要添加一个解决 粘包 问题的处理器
                        ch.pipeline()
                                .addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 6, 4, 0, 0))
                                .addLast(new TcpMsgCodec())
                                .addLast(handler);
                    }
                });

        // 在 JVM 关闭的时候，将 clientGroup 也关闭掉
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            clientGroup.shutdownGracefully();
            log.info("{} stopped...", clientName);
        }));

        // 连接服务器
        try {
            Channel channel = bootstrap.connect(ip, port).sync().channel();
            log.info("{} started...", clientName);
            return channel;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 开启 netty 的客户端，与服务端建立连接就会返回
     *
     * @param serverAddress 待连接的服务端的 ip 地址 和 端口号
     * @param clientName    客户端名称，用于打印日志
     * @param handler       客户端对 TcpMsg 的处理器，注意：这里的 handler 要求实现 @ChannelHandler.Sharable 接口
     * @return 返回与服务端通信的 channel
     */
    public static Channel startNettyClient(String serverAddress, String clientName,
                                           SimpleChannelInboundHandler<TcpMsg> handler) {
        String[] split = serverAddress.split(":");
        String ip = split[0];
        int port = Integer.parseInt(split[1]);
        return startNettyClient(ip, port, clientName, handler);
    }

    // 检查 handler 是否实现 @ChannelHandler.Sharable 接口，如果没有实现，则会抛出异常
    private static void checkHandler(SimpleChannelInboundHandler<TcpMsg> handler) {
        AssertUtil.isTrue(handler.getClass().isAnnotationPresent(ChannelHandler.Sharable.class),
                handler.getClass().getSimpleName() + " is not sharable");
    }
}
